<!DOCTYPE html>
<html>
<head>
<title>SAGE_TITLE</title>
<meta charset="utf-8">
<meta name=viewport content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>

    body { margin: 0px; overflow: hidden; }

    #menu-container { position: absolute; bottom: 30px; right: 40px; cursor: default; }

    #menu-message { position: absolute; bottom: 0px; right: 0px; white-space: nowrap;
                    display: none; background-color: #F5F5F5; padding: 10px; }

    #menu-content { position: absolute; bottom: 0px; right: 0px;
                    display: none; background-color: #F5F5F5; border-bottom: 1px solid black;
                    border-right: 1px solid black; border-left: 1px solid black; }

    #menu-content div { border-top: 1px solid black; padding: 10px; white-space: nowrap; }

    #menu-content div:hover { background-color: #FEFEFE; }

    .dark-theme #menu-container { color: white; }

    .dark-theme #menu-message { background-color: #181818; }

    .dark-theme #menu-content { background-color: #181818; border-color: white; }

    .dark-theme #menu-content div { border-color: white; }

    .dark-theme #menu-content div:hover { background-color: #303030; }

</style>
SAGE_STYLES
</head>

<body>
SAGE_SCRIPTS
<script>

    var options = SAGE_OPTIONS;
    var animate = options.animate;

    if ( options.theme === 'dark' )
        document.body.className = 'dark-theme';

    var scene = new THREE.Scene();

    var renderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true } );
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.setClearColor( options.theme === 'dark' ? 0 : 0xffffff, 1 );
    document.body.appendChild( renderer.domElement );

    var b = SAGE_BOUNDS; // bounds

    if ( b[0].x === b[1].x ) {
        b[0].x -= 1;
        b[1].x += 1;
    }
    if ( b[0].y === b[1].y ) {
        b[0].y -= 1;
        b[1].y += 1;
    }
    if ( b[0].z === b[1].z ) {
        b[0].z -= 1;
        b[1].z += 1;
    }

    var rRange = Math.sqrt( Math.pow( b[1].x - b[0].x, 2 )
                            + Math.pow( b[1].y - b[0].y, 2 ) );
    var xRange = b[1].x - b[0].x;
    var yRange = b[1].y - b[0].y;
    var zRange = b[1].z - b[0].z;

    var ar = options.aspectRatio;
    var a = [ ar[0], ar[1], ar[2] ]; // aspect multipliers
    var autoAspect = 2.5;
    if ( zRange > autoAspect * rRange && a[2] === 1 ) a[2] = autoAspect * rRange / zRange;

    // Distance from (xMid,yMid,zMid) to any corner of the bounding box, after applying aspectRatio
    var midToCorner = Math.sqrt( a[0]*a[0]*xRange*xRange + a[1]*a[1]*yRange*yRange + a[2]*a[2]*zRange*zRange ) / 2;

    var xMid = ( b[0].x + b[1].x ) / 2;
    var yMid = ( b[0].y + b[1].y ) / 2;
    var zMid = ( b[0].z + b[1].z ) / 2;

    var box = new THREE.Geometry();
    box.vertices.push( new THREE.Vector3( a[0]*b[0].x, a[1]*b[0].y, a[2]*b[0].z ) );
    box.vertices.push( new THREE.Vector3( a[0]*b[1].x, a[1]*b[1].y, a[2]*b[1].z ) );
    var boxMesh = new THREE.Line( box );
    var boxColor = options.theme === 'dark' ? 'white' : 'black';
    if ( options.frame ) scene.add( new THREE.BoxHelper( boxMesh, boxColor ) );

    if ( options.axesLabels ) {

        var d = options.decimals; // decimals
        var offsetRatio = 0.1;
        var al = options.axesLabels;
        var als = options.axesLabelsStyle || [{}, {}, {}];

        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
        var xm = xMid.toFixed(d);
        if ( /^-0.?0*$/.test(xm) ) xm = xm.substr(1);
        addLabel( al[0] + '=' + xm, a[0]*xMid, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );
        addLabel( ( b[0].x ).toFixed(d), a[0]*b[0].x, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );
        addLabel( ( b[1].x ).toFixed(d), a[0]*b[1].x, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );

        var offset = offsetRatio * a[0]*( b[1].x - b[0].x );
        var ym = yMid.toFixed(d);
        if ( /^-0.?0*$/.test(ym) ) ym = ym.substr(1);
        addLabel( al[1] + '=' + ym, a[0]*b[1].x+offset, a[1]*yMid, a[2]*b[0].z, als[1] );
        addLabel( ( b[0].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[0].y, a[2]*b[0].z, als[1] );
        addLabel( ( b[1].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[1].y, a[2]*b[0].z, als[1] );

        var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
        var zm = zMid.toFixed(d);
        if ( /^-0.?0*$/.test(zm) ) zm = zm.substr(1);
        addLabel( al[2] + '=' + zm, a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*zMid, als[2] );
        addLabel( ( b[0].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[0].z, als[2] );
        addLabel( ( b[1].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[1].z, als[2] );

    }

    function addLabel( text, x, y, z, style ) {

        var color = style.color || 'black';
        var fontSize = style.fontSize || 14;
        var fontFamily = style.fontFamily || 'monospace';
        var fontStyle = style.fontStyle || 'normal';
        var fontWeight = style.fontWeight || 'normal';
        var opacity = style.opacity || 1;

        if ( options.theme === 'dark' )
            if ( color === 'black' || color === '#000000' )
                color = 'white';

        if ( Array.isArray( fontStyle ) ) {
            fontFamily = fontFamily.map( function( f ) {
                // Need to put quotes around fonts that have whitespace in their names.
                return /\s/.test( f ) ? '"' + f + '"' : f;
            }).join(', ');
        }

        var canvas = document.createElement( 'canvas' );
        var context = canvas.getContext( '2d' );
        var pixelRatio = Math.round( window.devicePixelRatio );

        // For example: italic bold 20px "Times New Roman", Georgia, serif
        var font = [fontStyle, fontWeight, fontSize + 'px', fontFamily].join(' ');

        context.font = font;
        var width = context.measureText( text ).width;
        var height = fontSize;

        // The dimensions of the canvas's underlying image data need to be powers
        // of two in order for the resulting texture to support mipmapping.
        canvas.width = THREE.MathUtils.ceilPowerOfTwo( width * pixelRatio );
        canvas.height = THREE.MathUtils.ceilPowerOfTwo( height * pixelRatio );

        // Re-compute the unscaled dimensions after the power of two conversion.
        width = canvas.width / pixelRatio;
        height = canvas.height / pixelRatio;

        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';

        context.scale( pixelRatio, pixelRatio );
        context.fillStyle = color;
        context.font = font; // Must be set again after measureText.
        context.textAlign = 'center';
        context.textBaseline = 'middle';
        context.fillText( text, width/2, height/2 );

        var texture = new THREE.Texture( canvas );
        texture.needsUpdate = true;

        var materialOptions = { map: texture, sizeAttenuation: false, depthWrite: false };
        if ( opacity < 1 ) {
            // Setting opacity=1 would cause the texture's alpha component to be
            // discarded, giving the text a black background instead of the
            // background being transparent.
            materialOptions.opacity = opacity;
        }
        var sprite = new THREE.Sprite( new THREE.SpriteMaterial( materialOptions ) );
        sprite.position.set( x, y, z );

        // Scaling factor, chosen somewhat arbitrarily so that the size of the text
        // is consistent with previously generated plots.
        var scale = 1/625;
        if ( options.projection === 'orthographic' ) {
            scale = midToCorner/256; // Needs to scale along with the plot itself.
        }
        sprite.scale.set( scale * width, scale * height, 1 );

        scene.add( sprite );

        return sprite;

    }

    if ( options.axes ) scene.add( new THREE.AxesHelper( Math.min( a[0]*b[1].x, a[1]*b[1].y, a[2]*b[1].z ) ) );

    var camera = createCamera();
    camera.up.set( 0, 0, 1 );
    camera.position.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );

    var offset = new THREE.Vector3( a[0]*xRange, a[1]*yRange, a[2]*zRange );

    if ( options.viewpoint ) {

        var aa = options.viewpoint;
        var axis = new THREE.Vector3( aa[0][0], aa[0][1], aa[0][2] ).normalize();
        var angle = aa[1] * Math.PI / 180;
        var q = new THREE.Quaternion().setFromAxisAngle( axis, angle ).inverse();

        offset.set( 0, 0, offset.length() );
        offset.applyQuaternion( q );

    }

    camera.position.add( offset );

    function createCamera() {

        var aspect = window.innerWidth / window.innerHeight;

        // Scale the near and far clipping planes along with the overall plot size.
        var nearClip = 0.01 * midToCorner;
        var farClip = 100 * midToCorner;

        if ( options.projection === 'orthographic' ) {
            var camera = new THREE.OrthographicCamera( -1, 1, 1, -1, -farClip, farClip );
            updateCameraAspect( camera, aspect );
            return camera;
        }

        return new THREE.PerspectiveCamera( 45, aspect, nearClip, farClip );

    }

    function updateCameraAspect( camera, aspect ) {

        if ( camera.isPerspectiveCamera ) {
            camera.aspect = aspect;
        } else if ( camera.isOrthographicCamera ) {
            // Fit the camera frustum to the bounding box's diagonal so that the entire plot fits
            // within at the default zoom level and camera position.
            if ( aspect > 1 ) { // Wide window
                camera.top = midToCorner;
                camera.right = midToCorner * aspect;
            } else { // Tall or square window
                camera.top = midToCorner / aspect;
                camera.right = midToCorner;
            }
            camera.bottom = -camera.top;
            camera.left = -camera.right;
        }

        camera.updateProjectionMatrix();

    }

    var lights = SAGE_LIGHTS;
    for ( var i=0 ; i < lights.length ; i++ ) {
        var light = new THREE.DirectionalLight( lights[i].color, 1 );
        light.position.set( a[0]*lights[i].x, a[1]*lights[i].y, a[2]*lights[i].z );
        if ( lights[i].parent === 'camera' ) {
            light.target.position.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
            scene.add( light.target );
            camera.add( light );
        } else scene.add( light );
    }
    scene.add( camera );

    var ambient = SAGE_AMBIENT;
    scene.add( new THREE.AmbientLight( ambient.color, 1 ) );

    var controls = new THREE.OrbitControls( camera, renderer.domElement );
    controls.target.set( a[0]*xMid, a[1]*yMid, a[2]*zMid );
    controls.addEventListener( 'change', function() { if ( !animate ) render(); } );

    window.addEventListener( 'resize', function() {

        renderer.setSize( window.innerWidth, window.innerHeight );
        updateCameraAspect( camera, window.innerWidth / window.innerHeight );
        if ( window.rescaleFatLines ) rescaleFatLines();
        if ( !animate ) render();

    } );

    var texts = SAGE_TEXTS;
    for ( var i=0 ; i < texts.length ; i++ ) addText( texts[i] );

    function addText( json ) {
        var sprite = addLabel( json.text, a[0]*json.x, a[1]*json.y, a[2]*json.z, json );
        sprite.userData = json;
    }

    var points = SAGE_POINTS;
    for ( var i=0 ; i < points.length ; i++ ) addPoint( points[i] );

    function addPoint( json ) {

        var geometry = new THREE.Geometry();
        var v = json.point;
        geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );

        var canvas = document.createElement( 'canvas' );
        canvas.width = 128;
        canvas.height = 128;

        var context = canvas.getContext( '2d' );
        context.arc( 64, 64, 64, 0, 2 * Math.PI );
        context.fillStyle = json.color;
        context.fill();

        var texture = new THREE.Texture( canvas );
        texture.needsUpdate = true;

        var transparent = json.opacity < 1 ? true : false;
        var size = camera.isOrthographicCamera ? json.size : json.size/100;
        var material = new THREE.PointsMaterial( { size: size, map: texture,
                                                   transparent: transparent, opacity: json.opacity,
                                                   alphaTest: .1 } );

        var c = new THREE.Vector3();
        geometry.computeBoundingBox();
        geometry.boundingBox.getCenter( c );
        geometry.translate( -c.x, -c.y, -c.z );

        var mesh = new THREE.Points( geometry, material );
        mesh.position.set( c.x, c.y, c.z );
        mesh.userData = json;
        scene.add( mesh );

    }

    var lines = SAGE_LINES;
    for ( var i=0 ; i < lines.length ; i++ ) addLine( lines[i] );

    function addLine( json ) {

        var geometry = new THREE.Geometry();
        for ( var i=0 ; i < json.points.length ; i++ ) {
            var v = json.points[i];
            geometry.vertices.push( new THREE.Vector3( a[0]*v[0], a[1]*v[1], a[2]*v[2] ) );
        }

        var c = new THREE.Vector3();
        geometry.computeBoundingBox();
        geometry.boundingBox.getCenter( c );
        geometry.translate( -c.x, -c.y, -c.z );

        var transparent = json.opacity < 1 ? true : false;
        var materialOptions = { color: json.color, linewidth: json.linewidth,
                                transparent: transparent, opacity: json.opacity };

        var mesh;
        if ( json.linewidth > 1 && window.createFatLineStrip ) {
            mesh = createFatLineStrip( geometry, materialOptions );
        } else {
            var material = new THREE.LineBasicMaterial( materialOptions );
            mesh = new THREE.Line( geometry, material );
        }

        mesh.position.set( c.x, c.y, c.z );
        mesh.userData = json;
        scene.add( mesh );

    }

    var surfaces = SAGE_SURFACES;
    for ( var i=0 ; i < surfaces.length ; i++ ) addSurface( surfaces[i] );

    function addSurface( json ) {

        var useFaceColors = 'faceColors' in json ? true : false;

        var geometry = new THREE.Geometry();
        for ( var i=0 ; i < json.vertices.length ; i++ ) {
            var v = json.vertices[i];
            geometry.vertices.push( new THREE.Vector3( a[0]*v.x, a[1]*v.y, a[2]*v.z ) );
        }
        for ( var i=0 ; i < json.faces.length ; i++ ) {
            var f = json.faces[i];
            for ( var j=0 ; j < f.length - 2 ; j++ ) {
                var face = new THREE.Face3( f[0], f[j+1], f[j+2] );
                if ( useFaceColors ) face.color.set( json.faceColors[i] );
                geometry.faces.push( face );
            }
        }
        geometry.computeVertexNormals();

        var side = json.singleSide ? THREE.FrontSide : THREE.DoubleSide;
        var transparent = json.opacity < 1 ? true : false;
        var depthWrite = 'depthWrite' in json ? json.depthWrite : !transparent;
        var flatShading = json.useFlatShading ? json.useFlatShading : false;

        var material = new THREE.MeshPhongMaterial( { side: side,
                                     color: useFaceColors ? 'white' : json.color,
                                     vertexColors: useFaceColors ? THREE.FaceColors : THREE.NoColors,
                                     transparent: transparent, opacity: json.opacity,
                                     shininess: 20, flatShading: flatShading,
                                     depthWrite: depthWrite } );

        var c = new THREE.Vector3();
        geometry.computeBoundingBox();
        geometry.boundingBox.getCenter( c );
        geometry.translate( -c.x, -c.y, -c.z );

        var mesh = new THREE.Mesh( geometry, material );
        mesh.position.set( c.x, c.y, c.z );
        if ( transparent && json.renderOrder ) mesh.renderOrder = json.renderOrder;
        mesh.userData = json;
        scene.add( mesh );

        if ( json.showMeshGrid ) addSurfaceMeshGrid( json );

    }

    function addSurfaceMeshGrid( json ) {

        var geometry = new THREE.Geometry();

        for ( var i=0 ; i < json.faces.length ; i++ ) {
            var f = json.faces[i];
            for ( var j=0 ; j < f.length ; j++ ) {
                var k = j === f.length-1 ? 0 : j+1;
                var v1 = json.vertices[f[j]];
                var v2 = json.vertices[f[k]];
                // vertices in opposite directions on neighboring faces
                var nudge = f[j] < f[k] ? .0005*zRange : -.0005*zRange;
                geometry.vertices.push( new THREE.Vector3( a[0]*v1.x, a[1]*v1.y, a[2]*(v1.z+nudge) ) );
                geometry.vertices.push( new THREE.Vector3( a[0]*v2.x, a[1]*v2.y, a[2]*(v2.z+nudge) ) );
            }
        }

        var c = new THREE.Vector3();
        geometry.computeBoundingBox();
        geometry.boundingBox.getCenter( c );
        geometry.translate( -c.x, -c.y, -c.z );

        var gridColor = options.theme === 'dark' ? 'white' : 'black';
        var linewidth = json.linewidth || 1;
        var materialOptions = { color: gridColor, linewidth: linewidth };

        var mesh;
        if ( linewidth > 1 && window.createFatLineSegments ) {
            mesh = createFatLineSegments( geometry, materialOptions );
        } else {
            var material = new THREE.LineBasicMaterial( materialOptions );
            mesh = new THREE.LineSegments( geometry, material );
        }

        mesh.position.set( c.x, c.y, c.z );
        mesh.userData = json;
        scene.add( mesh );

    }

    function render() {

        if ( window.updateAnimation ) animate = updateAnimation();
        if ( animate ) requestAnimationFrame( render );

        renderer.render( scene, camera );

    }

    render();
    controls.update();
    if ( !animate ) render();


    // menu functions

    function toggleMenu() {

        var m = document.getElementById( 'menu-content' );
        if ( m.style.display === 'block' ) m.style.display = 'none'
        else m.style.display = 'block';

    }


    function saveAsPNG() {

        var a = document.body.appendChild( document.createElement( 'a' ) );
        a.href = renderer.domElement.toDataURL( 'image/png' );
        a.download = 'screenshot';
        a.click();

    }

    function saveAsHTML() {

        toggleMenu(); // otherwise visible in output
        event.stopPropagation();

        var blob = new Blob( [ '<!DOCTYPE html>\n' + document.documentElement.outerHTML ] );
        var a = document.body.appendChild( document.createElement( 'a' ) );
        a.href = window.URL.createObjectURL( blob );
        a.download = suggestFilename();
        a.click();

        function suggestFilename() {
            if ( !document.title ) {
                return 'graphic.html';
            } else if ( /\.html?$/i.test( document.title ) ) {
                return document.title; // already ends in .htm or .html
            } else {
                return document.title + '.html';
            }
        }

    }

    function getViewpoint() {

        function roundTo( x, n ) { return +x.toFixed(n); }

        var v = camera.quaternion.inverse();
        var r = Math.sqrt( v.x*v.x + v.y*v.y + v.z*v.z );
        var axis = [ roundTo( v.x / r, 4 ), roundTo( v.y / r, 4 ), roundTo( v.z / r, 4 ) ];
        var angle = roundTo( 2 * Math.atan2( r, v.w ) * 180 / Math.PI, 2 );

        var textArea = document.createElement( 'textarea' );
        textArea.textContent = JSON.stringify( axis ) + ',' + angle;
        textArea.style.csstext = 'position: absolute; top: -100%';
        document.body.append( textArea );
        textArea.select();
        document.execCommand( 'copy' );

        var m = document.getElementById( 'menu-message' );
        m.innerHTML = 'Viewpoint copied to clipboard';
        m.style.display = 'block';
        setTimeout( function() { m.style.display = 'none'; }, 2000 );

    }

    function getCamera() {

        function roundTo( x, n ) { return +x.toFixed(n); }

        var pos = camera.position;
        var pos_r = [ roundTo( pos.x, 4 ), roundTo( pos.y, 4 ), roundTo( pos.z, 4 ) ];
   //     var up = camera.up; // up is always (0,0,1)
        var textArea = document.createElement('textarea');
        var cam_position = JSON.stringify(pos_r);
        textArea.textContent = ',camera_position=' + cam_position;
        textArea.style.csstext = 'position: absolute; top: -100%';
        document.body.append( textArea );
        textArea.select();
        document.execCommand( 'copy' );

        var m = document.getElementById( 'menu-message' );
        m.innerHTML = 'Camera position '+ cam_position+' copied to clipboard';
        m.style.display = 'block';
        setTimeout( function() { m.style.display = 'none'; }, 2000 );

    }
                                       
</script>

<div id="menu-container" onclick="toggleMenu()">&#x24d8;
<div id="menu-message"></div>
<div id="menu-content">
<div onclick="saveAsPNG()">Save as PNG</div>
<div onclick="saveAsHTML()">Save as HTML</div>
<div onclick="getCamera()">Get camera</div>
<div onclick="getViewpoint()">Get viewpoint</div>
<div>Close Menu</div>
</div></div>

SAGE_EXTRA_HTML
</body>
</html>
